﻿using System;
using System.Collections.Generic;
using System.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace RemoteTestRunner.UnitTesting
{
    /// <summary>
    /// Function level routine for tracking individual asserts in test methods that have multiple asserts.
    /// </summary>
    public class TestMethodHelper : IDisposable
    {
        #region Private Attributes

        private readonly TestContext _testContext;
        private readonly Dictionary<string, object> _uniqueAsserts;
        private int _failureCount;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="TestMethodHelper"/> class.
        /// </summary>
        /// <param name="testContext">The test context.</param>
        internal TestMethodHelper(TestContext testContext)
        {
            // By default, treat empty strings and nulls as equivelent when comparing
            TreatEmptyStringAsNullString = true;

            _uniqueAsserts = new Dictionary<string, object>();
            Asserts = new List<AssertResult>();
            Inputs = new List<Input>();

            _testContext = testContext;
            _failureCount = 0;
            TestMethodName = testContext.TestName;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets a value indicating whether and empty string should be considered
        /// equivelent to a null string for comparison purposes.
        /// </summary>
        public bool TreatEmptyStringAsNullString { get; set; }

        #endregion

        #region Internal Properties

        /// <summary>
        /// Gets the inputs.
        /// </summary>
        /// <value>The inputs.</value>
        internal IList<Input> Inputs { get; private set; }

        /// <summary>
        /// Gets the name of the test method.
        /// </summary>
        /// <value>The name of the test method.</value>
        internal string TestMethodName { get; private set; }

        /// <summary>
        /// Gets the asserts.
        /// </summary>
        /// <value>The asserts.</value>
        internal IList<AssertResult> Asserts { get; private set; }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Gets the enum value for the object passed in.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnValue">The column value.</param>
        /// <param name="ignoreCase">if set to <c>true</c> [ignore case].</param>
        /// <param name="defaultIfNullOrEmpty">The default if null or empty.</param>
        /// <returns></returns>
        internal static T EnumParseInternal<T>(object columnValue, bool ignoreCase, T defaultIfNullOrEmpty)
        {
            if (columnValue == DBNull.Value || columnValue == null)
            {
                return defaultIfNullOrEmpty;
            }

            var fieldStringValue = columnValue.ToString();
            // Remove spaces since Enums dont have spaces.
            fieldStringValue = fieldStringValue.Replace(" ", "");
            if (string.IsNullOrEmpty(fieldStringValue))
            {
                return defaultIfNullOrEmpty;
            }

            return (T)Enum.Parse(typeof(T), fieldStringValue, ignoreCase);
        }

        /// <summary>
        /// Gets the typed value passed in object.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnValue">The column value.</param>
        /// <returns></returns>
        internal static T FieldValueInternal<T>(object columnValue)
        {
            if (columnValue == DBNull.Value || columnValue == null)
            {
                return default(T);
            }

            // Try casting first, then try parsing.
            try
            {
                return CastHelperInternal<T>(columnValue);
            }
            catch (InvalidCastException)
            {
                // Cast failed, parse the expected.
                return ParseInternal<T>(columnValue);
            }
        }

        /// <summary>
        /// Casts input object into type T.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnValue">The column value.</param>
        /// <returns></returns>
        internal static T CastHelperInternal<T>(object columnValue)
        {
            if (columnValue == DBNull.Value || columnValue == null)
            {
                return default(T);
            }

            // Allow decimal values to move through easier.
            switch (typeof(T).ToString())
            {
                case "System.Double":
                case "System.Nullable`1[System.Double]":
                    return (T)(object)Convert.ToDouble(columnValue);
                case "System.Decimal":
                case "System.Nullable`1[System.Decimal]":
                    return (T)(object)Convert.ToDecimal(columnValue);
                case "System.Int16":
                case "System.Nullable`1[System.Int16]":
                    return (T)(object)Convert.ToInt16(columnValue);
                case "System.Int32":
                case "System.Nullable`1[System.Int32]":
                    return (T)(object)Convert.ToInt32(columnValue);
                case "System.Int64":
                case "System.Nullable`1[System.Int64]":
                    return (T)(object)Convert.ToInt64(columnValue);
                case "System.Guid":
                case "System.Nullable`1[System.Guid]":
                    return (T)(object)new Guid(columnValue.ToString());
                case "System.Boolean":
                case "System.Nullable`1[System.Boolean]":
                    return (T)(object)Convert.ToBoolean(columnValue);
            }

            return (T)columnValue;
        }

        /// <summary>
        /// Internal implementation of AreEqual that runs the test through the Microsoft assert.
        /// </summary>
        /// <typeparam name="T">The type of the values to be compared.</typeparam>
        /// <param name="expected">The expected.</param>
        /// <param name="actual">The actual.</param>
        /// <param name="assertResult">The assert result.</param>
        /// <returns>Returns the assert result.</returns>
        internal void AreEqualInternal<T>(T expected, T actual, AssertResult assertResult)
        {
            try
            {
                if (_uniqueAsserts.ContainsKey(assertResult.AssertName))
                {
                    throw new ArgumentException("Assert names must be unique within a test method.", "assertResult");
                }

                _uniqueAsserts.Add(assertResult.AssertName, null);

                // Call the Microsoft Assert.AreEqual
                if (TreatEmptyStringAsNullString &&
                    typeof(T) == typeof(string) &&
                    (Equals(expected, default(T)) || string.IsNullOrEmpty(expected.ToString())) &&
                    (Equals(actual, default(T)) || string.IsNullOrEmpty(actual.ToString())))
                {
                    Assert.IsTrue(true, assertResult.Message);
                }
                else
                {
                    Assert.AreEqual(expected, actual, assertResult.Message);
                }

                assertResult.TestPassed = true;
            }
            catch (AssertFailedException ex)
            {
                assertResult.TestPassed = false;
                _failureCount++;
                _testContext.WriteLine("Assert failed: " + ex.Message);
            }
            catch (Exception ex)
            {
                assertResult.TestPassed = false;
                assertResult.ResultException = ex;
                _failureCount++;
                _testContext.WriteLine("Assert failed: " + ex.Message);
            }

            //return assertResult;
        }

        #endregion

        #region Adding Inputs

        /// <summary>
        /// Adds an input column that will be used as a basis for comparison.
        /// </summary>
        /// <param name="columnName">The name of the column being added as an input.</param>
        /// <param name="value">The value of the input column.</param>
        /// <returns>A reference to this object.</returns>
        public TestMethodHelper AddInput(string columnName, object value)
        {
            Inputs.Add(new Input
                           {
                               Name = columnName,
                               Value = value.ToString(),
                               Missing = false
                           });
            return this;
        }

        /// <summary>
        /// Adds an input column that will be used as a basis for comparison.
        /// </summary>
        /// <param name="columnName">The name of the column being added as an input.</param>
        /// <returns>A reference to this object.</returns>
        /// <remarks>The value of the input column will be initialized to the corresponding column
        /// in the associated test data source.</remarks>
        public TestMethodHelper AddInput(string columnName)
        {
            if (ColumnExists(columnName))
            {
                Inputs.Add(new Input
                {
                    Name = columnName,
                    Value = _testContext.DataRow[columnName].ToString(),
                    Missing = false
                });
            }
            else
            {
                Inputs.Add(new Input
                {
                    Name = columnName,
                    Value = "missing",
                    Missing = true
                });
            }

            return this;
        }

        /// <summary>
        /// Adds an input column that will be used as a basis for comparison.
        /// </summary>
        /// <typeparam name="T">The data type of the referenced input column.</typeparam>
        /// <param name="columnName">The name of the column being added as an input.</param>
        /// <param name="destination">The destination for the value of the input after it is read from the
        /// test data source.</param>
        /// <returns>A reference to this object.</returns>
        public TestMethodHelper AddInput<T>(string columnName, ref T destination)
        {
            if (ColumnExists(columnName))
            {
                var columnValue = _testContext.DataRow[columnName];

                Inputs.Add(new Input { Name = columnName, Value = columnValue.ToString(), Missing = false });
                destination = FieldValueInternal<T>(columnValue);
            }
            else
            {
                Inputs.Add(new Input { Name = columnName, Value = "missing", Missing = true });
            }

            return this;
        }

        /// <summary>
        /// Adds all data source columns as input columns to be used as a basis for comparison.
        /// </summary>
        /// <param name="excludedSuffixFilter">A string value that, when used as a suffix, will determine
        /// which columns should not be included as an input.  For example, specifying "?" will include
        /// only those columns that do not end with a "?" - e.g. "MemberCount?" would not be included.</param>
        /// <returns>A reference to this object.</returns>
        /// <remarks>The value of each input column will be initialized to the corresponding column
        /// in the associated test data source.
        /// <para/>A typical approach defines all input columns without a suffix and all output with a 
        /// suffix (e.g. "?").</remarks>
        public TestMethodHelper AddInputs(string excludedSuffixFilter)
        {
            // Add each column in the schema, assuming it matches the current filter criteria
            foreach (DataColumn column in _testContext.DataRow.Table.Columns)
            {
                if (string.IsNullOrEmpty(excludedSuffixFilter) || !column.ColumnName.EndsWith(excludedSuffixFilter))
                {
                    var found = false;

                    // Let's make sure the column hasn't already been added
                    foreach (var input in Inputs)
                    {
                        if (input.Name.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase))
                        {
                            found = true;
                            break;
                        }
                    }

                    // The column wasn't found so let's add it
                    if (!found)
                    {
                        Inputs.Add(new Input
                                       {
                                           Name = column.ColumnName,
                                           Value = _testContext.DataRow[column.ColumnName].ToString(),
                                           Missing = false
                                       });
                    }
                }
            }

            return this;
        }

        #endregion

        #region Adding Outputs

        /// <summary>
        /// Adds a column to the list of outputs.
        /// </summary>
        /// <typeparam name="T">The data type of the <paramref name="actual"/> value.</typeparam>
        /// <param name="actual">The actual value to be associated with this output column.</param>
        /// <param name="message"> A message to display if the assertion fails. This message can be seen in the 
        /// unit test results.</param>
        /// <param name="columnName">Specifies the name of the output column.</param>
        /// <returns>A reference to this object.</returns>
        public TestMethodHelper AddOutput<T>(T actual, string message, string columnName)
        {
            var assertResult = new AssertResult(string.Empty, actual.ToString(), columnName, message, false)
                                   {
                                       OutputOnly = true,
                                       TestPassed = true
                                   };

            Asserts.Add(assertResult);

            return this;
        }

        /// <summary>
        /// Adds all data source columns as output columns if they have not already been added as an input column
        /// or included in an assertion.
        /// </summary>
        /// <param name="includedSuffixFilter">A string value that, when used as a suffix, will determine
        /// which columns should be added as an output.  For example, specifying "?" will include only those
        /// columns ending in "?" - e.g. "MemberCount?" would be included.</param>
        /// <returns>A reference to this object.</returns>
        /// <remarks>The value of each output column will be initialized to the corresponding column
        /// in the associated test data source.
        /// <para/>A typical approach defines all input columns without a suffix and all output with a 
        /// suffix (e.g. "?").</remarks>
        public TestMethodHelper AddOutputs(string includedSuffixFilter)
        {
            return AddOutputs(includedSuffixFilter, string.Empty);
        }

        /// <summary>
        /// Adds all data source columns as output columns if they have not already been added as an input column
        /// or included in an assertion.
        /// </summary>
        /// <param name="includedSuffixFilter">A string value that, when used as a suffix, will determine
        /// which columns should be added as an output.  For example, specifying "?" will include only those
        /// columns ending in "?" - e.g. "MemberCount?" would be included.</param>
        /// <param name="newSuffix">If specified, provides an additional suffix that will be appended to
        /// the column name when added to the results.  This helps to visually indicate non-referenced
        /// columns.</param>
        /// <returns>A reference to this object.</returns>
        /// <remarks>The value of each output column will be initialized to the corresponding column
        /// in the associated test data source.
        /// <para/>A typical approach defines all input columns without a suffix and all output with a 
        /// suffix (e.g. "?").</remarks>
        public TestMethodHelper AddOutputs(string includedSuffixFilter, string newSuffix)
        {
            // Add each column in the schema, assuming it matches the current filter criteria
            foreach (DataColumn column in _testContext.DataRow.Table.Columns)
            {
                if (string.IsNullOrEmpty(includedSuffixFilter) || column.ColumnName.EndsWith(includedSuffixFilter))
                {
                    var found = false;

                    // Let's make sure the column hasn't already been added as an input column
                    foreach (var input in Inputs)
                    {
                        if (input.Name.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase))
                        {
                            found = true;
                            break;
                        }
                    }

                    // Let's make sure the column hasn't already been used in an assertion
                    if (!found)
                    {
                        foreach (var assert in Asserts)
                        {
                            if (assert.AssertName.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase))
                            {
                                found = true;
                                break;
                            }
                        }
                    }

                    // The column wasn't found so let's add it
                    if (!found)
                    {
                        AddOutput(_testContext.DataRow[column.ColumnName].ToString(), column.ColumnName,
                                  column.ColumnName + newSuffix ?? string.Empty);
                    }
                }
            }

            return this;
        }

        #endregion

        #region Assert methods

        /// <summary>
        /// Verifies that the actual value and the data source value specified by <paramref name="expectedColumnName"/>
        /// are equal. The assertion fails if the two values are not equal.
        /// </summary>
        /// <typeparam name="T">The data type of the <paramref name="actual"/> value.</typeparam>
        /// <param name="actual">The second value to compare. This is the value the unit test produced.</param>
        /// <param name="message"> A message to display if the assertion fails. This message can be seen in the 
        /// unit test results.</param>
        /// <param name="expectedColumnName">Specifies the column name of the first value to compare.  This is
        /// the value the unit test expects.</param>
        /// <returns>A reference to this object.</returns>
        public TestMethodHelper AreEqual<T>(T actual, string message, string expectedColumnName)
        {
            object columnValue;
            AssertResult assertResult;

            if (ColumnExists(expectedColumnName))
            {
                columnValue = _testContext.DataRow[expectedColumnName];

                assertResult = new AssertResult(
                    columnValue == null ? "<null>" : columnValue.ToString(),
                    Equals(actual, default(T)) ? "<null>" : actual.ToString(),
                    expectedColumnName,
                    message,
                    false);

                Asserts.Add(assertResult);

                try
                {
                    var expected = FieldValueInternal<T>(columnValue);

                    AreEqualInternal(expected, actual, assertResult);
                }
                catch (Exception ex)
                {
                    // An exception can occur when casting/parsing to T.
                    assertResult.TestPassed = false;
                    assertResult.ResultException = ex;
                    _failureCount++;
                    _testContext.WriteLine("Assert failed: " + ex.Message);
                }
            }
            else
            {
                assertResult = new AssertResult("missing", actual.ToString(), expectedColumnName, message, true)
                                   {
                                       TestPassed = false
                                   };

                Asserts.Add(assertResult);
            }

            return this;
        }

        /// <summary>
        /// Verifies that the actual and expected values are equal. The assertion fails if the two values are not equal.
        /// </summary>
        /// <typeparam name="T">The data type of the <paramref name="actual"/> value.</typeparam>
        /// <param name="expected">The first value to compare. This is the value the unit test expects.</param>
        /// <param name="actual">The second value to compare. This is the value the unit test produced.</param>
        /// <param name="message"> A message to display if the assertion fails. This message can be seen in the 
        /// unit test results.</param>
        /// <param name="columnName">A unique name to be associated with this assertion - e.g. a data source column name.</param>
        /// <returns>A reference to this object.</returns>
        public TestMethodHelper AreEqual<T>(object expected, T actual, string message, string columnName)
        {
            var assertResult = new AssertResult(expected.ToString(), actual.ToString(), columnName, message, false);

            Asserts.Add(assertResult);

            try
            {
                var newExpected = CastHelperInternal<T>(expected);

                AreEqualInternal(newExpected, actual, assertResult);
            }
            catch (InvalidCastException ex)
            {
                // An exception can occur when casting to T.
                var newException =
                    new InvalidCastException(
                        "Cannot cast 'expected' type of " + expected.GetType().Name + " to 'actual' type of " +
                        typeof(T).Name + ".",
                        ex);

                assertResult.TestPassed = false;
                assertResult.ResultException = newException;
                _failureCount++;
                _testContext.WriteLine("Assert failed: " + newException.Message);
            }
            catch (Exception ex)
            {
                // An exception can occur when casting to T.
                assertResult.TestPassed = false;
                assertResult.ResultException = ex;
                _failureCount++;
                _testContext.WriteLine("Assert failed: " + ex.Message);
            }

            return this;
        }

        /// <summary>
        /// Verifies that the actual and expected values are equal. The assertion fails if the two values are not equal.
        /// </summary>
        /// <typeparam name="T">The data type of the values to be compared.</typeparam>
        /// <param name="expected">The first value to compare. This is the value the unit test expects.</param>
        /// <param name="actual">The second value to compare. This is the value the unit test produced.</param>
        /// <param name="message"> A message to display if the assertion fails. This message can be seen in the 
        /// unit test results.</param>
        /// <param name="columnName">A unique name to be associated with this assertion - e.g. a data source column name.</param>
        /// <returns>A reference to this object.</returns>
        public TestMethodHelper AreEqual<T>(T expected, T actual, string message, string columnName)
        {
            var assertResult = new AssertResult(expected.ToString(), actual.ToString(), columnName, message, false);

            Asserts.Add(assertResult);
            AreEqualInternal(expected, actual, assertResult);

            return this;
        }

        /// <summary>
        /// Verifies that the specified condition is true. The assertion fails if the condition is false.
        /// </summary>
        /// <param name="message"> A message to display if the assertion fails. This message can be seen in the 
        /// unit test results.</param>
        /// <param name="columnName">The name of the column that contains the actual value.</param>
        /// <returns>A reference to this object.</returns>
        public TestMethodHelper IsTrue(string message, string columnName)
        {
            var actual = FieldValue<bool>(columnName);

            var assertResult = new AssertResult("true", actual.ToString(), columnName, message, false);

            Asserts.Add(assertResult);
            AreEqualInternal(true, actual, assertResult);

            return this;
        }

        /// <summary>
        /// Verifies that the specified condition is false. The assertion fails if the condition is true.
        /// </summary>
        /// <param name="message"> A message to display if the assertion fails. This message can be seen in the 
        /// unit test results.</param>
        /// <param name="columnName">The name of the column that contains the actual value.</param>
        /// <returns>A reference to this object.</returns>
        public TestMethodHelper IsFalse(string message, string columnName)
        {
            var actual = FieldValue<bool>(columnName);

            var assertResult = new AssertResult("false", actual.ToString(), columnName, message, false);

            Asserts.Add(assertResult);
            AreEqualInternal(false, actual, assertResult);

            return this;
        }

        #endregion

        #region Helper Methods

        /// <summary>
        /// Reads the value from the TestContext.DataRow and converts it into the requested type.  
        /// A cast is tried first and if that fails then a Parse is attempted.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnName">Name of the column.</param>
        /// <returns></returns>
        public T FieldValue<T>(string columnName)
        {
            var columnValue = _testContext.DataRow[columnName];
            return FieldValueInternal<T>(columnValue);
        }

        /// <summary>
        /// Reads the value from the TestContext.DataRow and converts it into the requested type.  
        /// A cast is tried first and if that fails then a Parse is attempted.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnName">Name of the column.</param>
        /// <param name="destination">The destination for the value of the input after it is read from the Data Source.</param>
        public void FieldValue<T>(string columnName, ref T destination)
        {
            if (ColumnExists(columnName))
            {
                destination = FieldValue<T>(columnName);
            }
        }

        /// <summary>
        /// Determines whether the current data row (within the <see cref="TestContext"/>) is null.
        /// </summary>
        /// <returns><c>true</c> if all columns within the current data row (within the <see cref="TestContext"/>)
        /// are null; otherwise, <c>false</c>.
        /// </returns>
        /// <remarks>This method is especially useful when testing data using Microsoft Excel as a data
        /// source because "phantom" rows tend to appear at the end of the test data.</remarks>
        public bool IsDataRowNull()
        {
            var isNull = true;

            for (var index = 0; index < _testContext.DataRow.Table.Columns.Count; index++)
            {
                if (_testContext.DataRow[index] != null && !Convert.IsDBNull(_testContext.DataRow[index]))
                {
                    isNull = false;
                    break;
                }
            }

            return isNull;
        }

        /// <summary>
        /// Determines if the specified column exists within <see cref="TestContext"/>.
        /// </summary>
        /// <param name="columnName">The name of the column to check for.</param>
        /// <returns><c>true</c> if the specified column exists; otherwise, <c>false</c>.</returns>
        public bool ColumnExists(string columnName)
        {
            return _testContext.DataRow.Table.Columns.Contains(columnName);
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (_failureCount > 0)
            {
                throw new AssertFailedException(_failureCount + " assert(s) failed.");
            }
        }

        #endregion

        #region Parse Helper

        /// <summary>
        /// Parses the  data row value specified column name.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnValue">The column value.</param>
        /// <returns></returns>
        internal static T ParseInternal<T>(object columnValue)
        {
            if (columnValue == DBNull.Value || columnValue == null)
            {
                return default(T);
            }

            Type[] parseParameters = { typeof(string) };
            if (typeof(T).IsEnum)
            {
                return EnumParseInternal(columnValue, true, default(T));
            }

            if (typeof(T) == typeof(string))
            {
                return (T)columnValue;
            }

            //if (typeof(T).Name == "Nullable)
            //{typeof(T).GetGenericArguments()
            //}

            var targetType = typeof(T);

            if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                targetType = typeof(T).GetGenericArguments()[0];
            }

            if (targetType.GetMethod("Parse", parseParameters) != null)
            {
                // The type T has a parse method.  Use it.
                var parseMethod = targetType.GetMethod("Parse", parseParameters);
                object[] parameters = { columnValue.ToString() };
                return (T)parseMethod.Invoke(targetType, parameters);
            }

            throw new InvalidOperationException("A parse method is not available for the type: " + typeof(T).Name + ".");
        }

        #endregion

        #region Enum Helper

        /// <summary>
        /// Parses the value in DataRow column into the enum value.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnName">Name of the column.</param>
        /// <param name="defaultIfNullOrEmpty">The default if null or empty.</param>
        /// <returns></returns>
        public T EnumParse<T>(string columnName, T defaultIfNullOrEmpty)
        {
            return EnumParse(columnName, false, defaultIfNullOrEmpty);
        }

        /// <summary>
        /// Parses the value in DataRow column into the enum value.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnName">Name of the column.</param>
        /// <param name="ignoreCase">if set to <c>true</c> [ignore case].</param>
        /// <param name="defaultIfNullOrEmpty">The default if null or empty.</param>
        /// <returns></returns>
        public T EnumParse<T>(string columnName, bool ignoreCase, T defaultIfNullOrEmpty)
        {
            var value = _testContext.DataRow[columnName];
            return EnumParseInternal(value, ignoreCase, defaultIfNullOrEmpty);
        }

        #endregion

        #region Nested type: AssertResult

        /// <summary>
        /// Container object for storing the details for each executed assert.
        /// </summary>
        internal class AssertResult
        {
            internal AssertResult(string expected, string actual, string assertName, string message, bool missing)
            {
                Expected = expected;
                Actual = actual;
                AssertName = assertName;
                Message = message;
                Missing = missing;
                OutputOnly = false;
            }

            public string Expected { get; set; }
            public string Actual { get; set; }
            public string AssertName { get; set; }
            public string Message { get; set; }
            public bool TestPassed { get; set; }
            public Exception ResultException { get; set; }
            public bool Missing { get; set; }
            public bool OutputOnly { get; set; }
        }

        #endregion

        #region Nested type: Input

        /// <summary>
        /// Container object for storing the details of an Input.
        /// </summary>
        internal class Input
        {
            public string Name { get; set; }
            public string Value { get; set; }
            public bool Missing { get; set; }
        }

        #endregion
    }
}